package org.dromara.common.doc.handler

import cn.hutool.core.io.IoUtil
import io.github.oshai.kotlinlogging.KotlinLogging
import io.swagger.v3.core.jackson.TypeNameResolver
import io.swagger.v3.core.util.AnnotationsUtils
import io.swagger.v3.oas.annotations.tags.Tags
import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.Operation
import io.swagger.v3.oas.models.Paths
import io.swagger.v3.oas.models.tags.Tag
import org.apache.commons.lang3.StringUtils
import org.dromara.common.core.utils.StreamUtils.toSet
import org.springdoc.core.customizers.OpenApiBuilderCustomizer
import org.springdoc.core.customizers.ServerBaseUrlCustomizer
import org.springdoc.core.properties.SpringDocConfigProperties
import org.springdoc.core.providers.JavadocProvider
import org.springdoc.core.service.OpenAPIService
import org.springdoc.core.service.SecurityService
import org.springdoc.core.utils.PropertyResolverUtils
import org.springframework.context.ApplicationContext
import org.springframework.core.annotation.AnnotatedElementUtils
import org.springframework.util.CollectionUtils
import org.springframework.web.method.HandlerMethod
import java.io.StringReader
import java.lang.reflect.Method
import java.util.*
import java.util.stream.Collectors
import java.util.stream.Stream

/**
 * 自定义 openapi 处理器
 * 对源码功能进行修改 增强使用
 * @updater LikeYouDo
 * @date 2025/1/13 14:42
 */
open class OpenApiHandler(
    openAPI: Optional<OpenAPI>,
    securityParser: SecurityService,
    springDocConfigProperties: SpringDocConfigProperties,
    propertyResolverUtils: PropertyResolverUtils,
    openApiBuilderCustomizers: Optional<List<OpenApiBuilderCustomizer>>,
    serverBaseUrlCustomizers: Optional<List<ServerBaseUrlCustomizer>>,
    javadocProvider: Optional<JavadocProvider>
) : OpenAPIService(
    openAPI,
    securityParser,
    springDocConfigProperties,
    propertyResolverUtils,
    openApiBuilderCustomizers,
    serverBaseUrlCustomizers,
    javadocProvider
) {
    companion object {
        private var log = KotlinLogging.logger { }

        /**
         * The Basic error controller.
         */
        private lateinit var basicErrorController: Class<*>
    }

    /**
     * The Security parser.
     */
    private val securityParser: SecurityService

    /**
     * The Mappings map.
     */
    private val mappingsMap: Map<String, Any> = HashMap()

    /**
     * The Springdoc tags.
     */
    private val springdocTags: Map<HandlerMethod, Tag> = HashMap()

    /**
     * The Open api builder customisers.
     */
    private var openApiBuilderCustomisers: Optional<List<OpenApiBuilderCustomizer>>

    /**
     * The server base URL customisers.
     */
    private var serverBaseUrlCustomizers: Optional<List<ServerBaseUrlCustomizer>>

    /**
     * The Spring doc config properties.
     */
    private var springDocConfigProperties: SpringDocConfigProperties

    /**
     * The Cached open api map.
     */
    private val cachedOpenAPI: Map<String, OpenAPI> = HashMap()

    /**
     * The Property resolver utils.
     */
    private val propertyResolverUtils: PropertyResolverUtils

    /**
     * The javadoc provider.
     */
    private val javadocProvider: Optional<JavadocProvider>

    /**
     * The Context.
     */
    private lateinit var context: ApplicationContext

    /**
     * The Open api.
     */
    private lateinit var openAPI: OpenAPI

    /**
     * The Is servers present.
     */
    private var isServersPresent: Boolean = false

    /**
     * The Server base url.
     */
    private lateinit var serverBaseUrl: String

    /**
     * Instantiates a new Open api builder.
     */

    init {
        if (openAPI.isPresent) {
            this.openAPI = openAPI.get()
            if (this.openAPI.components == null)
                this.openAPI.components = Components()
            if (this.openAPI.paths == null)
                this.openAPI.paths = Paths()
            if (!CollectionUtils.isEmpty(this.openAPI.servers))
                this.isServersPresent = true
        }
        this.propertyResolverUtils = propertyResolverUtils
        this.securityParser = securityParser
        this.springDocConfigProperties = springDocConfigProperties
        this.openApiBuilderCustomisers = openApiBuilderCustomizers
        this.serverBaseUrlCustomizers = serverBaseUrlCustomizers
        this.javadocProvider = javadocProvider
        if (springDocConfigProperties.isUseFqn)
            TypeNameResolver.std.useFqn = true
    }

    override fun buildTags(
        handlerMethod: HandlerMethod,
        operation: Operation,
        openAPI: OpenAPI,
        locale: Locale
    ): Operation {
        val tags = mutableSetOf<Tag>()
        val tagsStr = mutableSetOf<String>()

        buildTagsFromMethod(handlerMethod.method, tags, tagsStr, locale)
        buildTagsFromClass(handlerMethod.beanType, tags, tagsStr, locale)

        if (tagsStr.isNotEmpty())
            tagsStr.stream()
                .map { str -> propertyResolverUtils.resolve(str, locale) }
                .collect(Collectors.toSet())

        if (springdocTags.containsKey(handlerMethod)) {
            val tag = springdocTags[handlerMethod]
            tag?.let {
                tagsStr.add(it.name)
                if (openAPI.tags == null || !openAPI.tags.contains(it))
                    openAPI.addTagsItem(it)
            }
        }

        if (tagsStr.isNotEmpty()) {
            if (operation.tags.isNullOrEmpty())
                operation.tags = tagsStr.toMutableList()
            else {
                val operationTagsSet = operation.tags.toMutableSet()
                operationTagsSet.addAll(tagsStr)
                operation.tags.clear()
                operation.tags.addAll(operationTagsSet)
            }
        }

        if (isAutoTagClasses(operation)) {
            if (javadocProvider.isPresent) {
                val description = javadocProvider.get().getClassJavadoc(handlerMethod.beanType)
                if (StringUtils.isNotBlank(description)) {
                    val tag = Tag()

                    // 自定义部分 修改使用java注释当tag名
                    val list: List<String> = IoUtil.readLines(StringReader(description), ArrayList())
                    // tag.setName(tagAutoName);
                    tag.name = list[0]
                    operation.addTagsItem(list[0])

                    tag.description = description
                    if (openAPI.tags == null || !openAPI.tags.contains(tag)) {
                        openAPI.addTagsItem(tag)
                    }
                }
            } else {
                val tagAutoName = OpenAPIService.splitCamelCase(handlerMethod.beanType.simpleName)
                operation.addTagsItem(tagAutoName)
            }
        }

        if (tags.isNotEmpty()) {
            // Existing tags
            val openApiTags = openAPI.tags
            if (openApiTags.isNotEmpty())
                tags.addAll(openApiTags)
            openAPI.tags = openApiTags
        }

        // Handle SecurityRequirement at operation level
        val securityRequirements = securityParser.getSecurityRequirements(handlerMethod)
        if (securityRequirements != null) {
            if (securityRequirements.isEmpty())
                operation.security = emptyList()
            else
                securityParser.buildSecurityRequirement(securityRequirements, operation)
        }

        return operation
    }

    private fun buildTagsFromMethod(
        method: Method,
        tags: MutableSet<Tag>,
        tagsStr: MutableSet<String>,
        locale: Locale
    ) {
        // method tags
        val tagsSet = AnnotatedElementUtils
            .findAllMergedAnnotations(method, Tags::class.java)
        val methodTags = tagsSet.stream()
            .flatMap { Stream.of(*it.value) }.collect(Collectors.toSet())
        methodTags.addAll(
            AnnotatedElementUtils.findAllMergedAnnotations(
                method,
                io.swagger.v3.oas.annotations.tags.Tag::class.java
            )
        )
        if (methodTags.isNotEmpty()) {
            tagsStr.addAll(toSet(methodTags) {
                propertyResolverUtils.resolve(it!!.name, locale)
            })
            val allTags = ArrayList(methodTags)
            addTags(allTags, tags, locale)
        }
    }

    private fun addTags(
        sourceTags: List<io.swagger.v3.oas.annotations.tags.Tag>,
        tags: MutableSet<Tag>,
        locale: Locale
    ) {
        val optionalTagSet = AnnotationsUtils
            .getTags(sourceTags.toTypedArray<io.swagger.v3.oas.annotations.tags.Tag>(), true)
        optionalTagSet.ifPresent { tagsSet ->
            tagsSet.forEach { tag ->
                tag.name(propertyResolverUtils.resolve(tag.name, locale))
                tag.description(propertyResolverUtils.resolve(tag.description, locale))
                if (tags.stream()
                        .noneMatch { t: Tag -> t.name == tag.name }
                ) tags.add(tag)
            }
        }
    }

}
